﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace GoodStuff
{
    /// <summary>
    /// Implementation of a very simple DI container that supports instance reuse.
    /// </summary>
    public class Container : ICanResolveDependencies
    {
        private Dictionary<RegistrationKey, Registration> _factories = new Dictionary<RegistrationKey, Registration>();
        private Dictionary<Registration, object> _instances = new Dictionary<Registration, object>();

        /// <summary>
        /// Registers a constructor for a service type with the container
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <param name="constructor"></param>
        public void Register<TService>(Func<Container, TService> constructor)
        {
            RegisterImpl<TService, Func<Container, TService>>(InstanceMode.CreateNew, constructor);
        }

        /// <summary>
        /// Registers a constructor for a service type with the container
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <param name="mode"></param>
        /// <param name="constructor"></param>
        public void Register<TService>(InstanceMode mode, Func<Container, TService> constructor)
        {
            RegisterImpl<TService, Func<Container, TService>>(mode, constructor);
        }

        /// <summary>
        /// Registers a constructor for a service type with the container
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <typeparam name="TArg"></typeparam>
        /// <param name="constructor"></param>
        public void Register<TService, TArg>(Func<Container, TArg, TService> constructor)
        {
            RegisterImpl<TService, Func<Container, TArg, TService>>(InstanceMode.CreateNew, constructor);
        }

        /// <summary>
        /// Registers a constructor for a service type with the container
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <typeparam name="TArg1"></typeparam>
        /// <typeparam name="TArg2"></typeparam>
        /// <param name="constructor"></param>
        public void Register<TService, TArg1, TArg2>(Func<Container, TArg1, TArg2, TService> constructor)
        {
            RegisterImpl<TService, Func<Container, TArg1, TArg2, TService>>(InstanceMode.CreateNew, constructor);
        }

        /// <summary>
        /// Returns the configured instance of a type
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        public TService Resolve<TService>()
        {
            return ResolveImp<TService, Func<Container, TService>>(factory => factory(this));
        }

        /// <summary>
        /// Returns the configured instance of a type
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <typeparam name="TArg"></typeparam>
        /// <param name="arg"></param>
        public TService Resolve<TService, TArg>(TArg arg)
        {
            return ResolveImp<TService, Func<Container, TArg, TService>>(factory => factory(this, arg));
        }

        /// <summary>
        /// Returns the configured instance of a type
        /// </summary>
        /// <typeparam name="TService"></typeparam>
        /// <typeparam name="TArg1"></typeparam>
        /// <typeparam name="TArg2"></typeparam>
        /// <param name="arg1"></param>
        /// <param name="arg2"></param>
        public TService Resolve<TService, TArg1, TArg2>(TArg1 arg1, TArg2 arg2)
        {
            return ResolveImp<TService, Func<Container, TArg1, TArg2, TService>>(factory => factory(this, arg1, arg2));

        }

        private void RegisterImpl<TService, TFunc>(InstanceMode mode, TFunc factory)
        {
            RegistrationKey key = new RegistrationKey(typeof(TService), typeof(TFunc));

            Registration entry = new Registration();
            entry.Factory = factory;
            entry.Mode = mode;

            _factories.Add(key, entry);
        }

        private TService ResolveImp<TService, TFunc>(Func<TFunc, TService> invoker)
        {
            RegistrationKey key = new RegistrationKey(typeof(TService), typeof(TFunc));

            if (_factories.ContainsKey(key))
            {
                Registration entry = _factories[key];
                TFunc factory = (TFunc)entry.Factory;

                switch (entry.Mode)
                {
                    case InstanceMode.CreateNew:
                        return invoker.Invoke(factory);
                    case InstanceMode.Reuse:
                        TService obj;
                        if (_instances.ContainsKey(entry))
                        {
                            obj = (TService)_instances[entry];
                        }
                        else
                        {
                            obj = invoker.Invoke(factory);
                            _instances.Add(entry, obj);
                        }
                        return obj;
                    default:
                        throw new ResolutionException("Unknown instancemode");
                }
            }
            throw new ResolutionException(string.Format("Type {0} not registered in container", typeof(TService).FullName));
        }

        public void ClearInstances()
        {
            _instances = new Dictionary<Registration, object>();
        }

        public void RegisterFromConfiguration<TService>()
        {
            this.Register<TService>(p => p.InjectFromConfiguration<TService>());
        }

        /// <summary>
        /// Injection from container only supports parameterless constructors.
        /// </summary>
        /// <typeparam name="TInstance"></typeparam>
        /// <returns></returns>
        public TInstance InjectFromConfiguration<TInstance>()
        {
            ContainerConfiguration config = ConfigurationHelper.GetSection<ContainerConfiguration>();
            ContainerConfigurationEntry entry = config.Types.SingleOrDefault(p => p.Interface == typeof(TInstance).FullName);
            if (entry == null)
            {
                throw new Exception("No configuration found for interface " + typeof(TInstance).FullName);
            }
            string typeName = entry.Type;

            Type t = Type.GetType(typeName, true);
            TInstance instance = (TInstance)Activator.CreateInstance(t);

            return instance;
        }

        private class RegistrationKey
        {
            public RegistrationKey(Type serviceType, Type factoryType)
            {
                ServiceType = serviceType;
                FactoryType = factoryType;

            }
            public Type ServiceType { get; private set; }
            public Type FactoryType { get; private set; }

            public override bool Equals(object obj)
            {
                return this.Equals((RegistrationKey)obj);
            }

            public bool Equals(RegistrationKey other)
            {
                return this.FactoryType == other.FactoryType && this.ServiceType == other.ServiceType;
            }

            public override int GetHashCode()
            {
                return ServiceType.GetHashCode() ^ FactoryType.GetHashCode();
            }
        }

        private class Registration
        {
            public InstanceMode Mode { get; set; }
            public object Factory { get; set; }
        }
    }

    /// <summary>
    /// Construction can be new for every request or reuse within container scope.
    /// </summary>
    public enum InstanceMode
    {
        /// <summary>
        /// Creates a new instance on every resolve.
        /// </summary>
        CreateNew,

        /// <summary>
        /// InstanceMode.Reuse will produce unpredictable results when used with constructor arguments.
        /// </summary>
        Reuse
    }

    /// <summary>
    /// Something went wrong where resolving a type.
    /// </summary>
    [global::System.Serializable]  
    public class ResolutionException : Exception
    {
        //
        // For guidelines regarding the creation of new exception types, see
        //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/cpgenref/html/cpconerrorraisinghandlingguidelines.asp
        // and
        //    http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dncscol/html/csharp07192001.asp
        //

        /// <summary>
        /// 
        /// </summary>
        public ResolutionException() { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        public ResolutionException(string message) : base(message) { }
        /// <summary>
        /// 
        /// </summary>
        /// <param name="message"></param>
        /// <param name="inner"></param>
        public ResolutionException(string message, Exception inner) : base(message, inner) { }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="info"></param>
        /// <param name="context"></param>
        protected ResolutionException(
          System.Runtime.Serialization.SerializationInfo info,
          System.Runtime.Serialization.StreamingContext context)
            : base(info, context) { }
    }
}

